/**
    @author: ChenLin
    @date: 2022/11/11 //todo 视频下载工具类
**/

package handler

import (
	"bufio"
	"bytes"
	"crypto/md5"
	"crypto/sha1"
	"encoding/gob"
	"errors"
	"flamingo/utils"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"math"
	"math/rand"
	"net/http"
	"os"
	"path"
	"strconv"
	"strings"
	"time"
)

type MultiThreadDownload struct {
	ThreadNum           int
	ThreadSize          int
	TimeOut             int
	Length              int
	Cache               bool
	FileName            string
	Hash                string
	Url                 string
	queue, redo, finish chan int
}

func MultiThreadInit(threadNum, threadSize, timeOut int, fileName, hash, url string) *MultiThreadDownload {
	var multi = &MultiThreadDownload{
		ThreadNum:  threadNum,
		ThreadSize: threadSize,
		TimeOut:    timeOut,
		FileName:   fileName,
		Hash:       hash,
		Url:        url,
	}
	return multi
}

func (m *MultiThreadDownload) HttpGetVideoByUrlV1(videoUrl, dirUrl string) (string, error) {
	m.Url = videoUrl
	fileName := path.Base(utils.MD5(time.Now().String() + strconv.Itoa(rand.Int())))
	m.FileName = dirUrl + fileName
	startTime := time.Now()
	client := http.Client{}
	request, err := http.NewRequest("GET", m.Url, nil)
	if err != nil {
		log.Println(err)
	}
	response, err := client.Do(request)
	response.Body.Close()
	num := response.Header.Get("Content-Length")
	m.Length, _ = strconv.Atoi(num)
	log.Println("Content-Length", m.Length)
	ranges := response.Header.Get("Accept-Ranges")
	if ranges != "bytes" || m.Length < 1000*1000*20 { //文件小于20M直接走单线程下载
		return m.HttpGetVideoByUrl(videoUrl, dirUrl)
	}
	log.Println("Ranges:", ranges)
	if m.ThreadSize <= 0 {
		m.ThreadSize = int(math.Ceil(float64(m.Length) / float64(m.ThreadNum)))
	}
	m.queue = make(chan int, m.ThreadNum)
	m.redo = make(chan int, m.ThreadNum)
	//go func() {
	//	for i := 0; i < fragment; i++ {
	//		m.queue <- i
	//	}
	//}()
	go func() {
		//todo redo是如果某块下载失败了，重新投递到queue，进而重新下载
		for {
			j := <-m.redo
			go m.DownloadBlock(request, m.ThreadNum, j)
			//m.queue <- j
		}
	}()
	m.finish = make(chan int, m.ThreadNum)
	for j := 0; j < m.ThreadNum; j++ {
		go m.DownloadBlock(request, m.ThreadNum, j)
	}
	//finish的目的：等分块fragment都完成了，主进程接着往下执行。如果没有这个，那么主进程不会等子协程结束就会提前退出
	for k := 0; k < m.ThreadNum; k++ {
		_ = <-m.finish
		//log.Printf("[%s][%d]Finished\n", "-", i)
	}
	log.Println("Start to combine files...")

	file, err := os.Create(m.FileName + ".mp4")
	if err != nil {
		log.Println(err)
		return "", err
	}
	defer file.Close()
	var offset int64 = 0

	//分块下载的多个文件，最后合并组装成一个
	for x := 0; x < m.ThreadNum; x++ {
		filename := fmt.Sprintf("%s_%d", m.FileName, x)
		buf, err := ioutil.ReadFile(filename)
		if err != nil {
			log.Println(err)
			continue
		}
		file.WriteAt(buf, offset)
		offset += int64(len(buf))
		os.Remove(filename)
	}
	log.Println("Written to ", m.finish)
	//hash
	if m.Hash == "sha1" {
		h := sha1.New()
		io.Copy(h, file)
		r := h.Sum(nil)
		log.Printf("sha1 of file: %x\n", r)
	} else if m.Hash == "md5" {
		h := md5.New()
		io.Copy(h, file)
		r := h.Sum(nil)
		log.Printf("sha1 of file: %x\n", r)
	}

	finishTime := time.Now()
	duration := finishTime.Sub(startTime).Seconds()
	log.Printf("Time:%f Speed:%f Kb/s\n", duration, float64(m.Length)/duration/1024)
	return fmt.Sprintf("%s%s", dirUrl, fileName+".mp4"), nil
}

/**
 * @Author ChenLin
 * @Description //TODO 分块下载
 * @Date 13:41 2022/11/25
 **/

func (m *MultiThreadDownload) DownloadBlock(request *http.Request, fragment, no int) {
	var req http.Request
	err := DeepCopy(&req, request)
	if err != nil {
		log.Println("ERROR|prepare request:", err)
		log.Panic(err)
		return
	}
	//for {
	cStartTime := time.Now()

	//i := <-m.queue
	//log.Printf("[%d][%d]Start download\n",no, i)
	start := no * m.ThreadSize
	var end int
	if no < fragment-1 {
		end = start + m.ThreadSize - 1
	} else {
		end = m.Length - 1
	}

	filename := fmt.Sprintf("%s_%d", m.FileName, no)
	if m.Cache {
		filesize := int64(end - start + 1)
		file, err := os.Stat(filename)
		if err == nil && file.Size() == filesize {
			log.Printf("[%d]Hint cached %s, size:%d\n", no, filename, filesize)
			m.finish <- no
			return
		}
	}

	req.Header.Set("Accept-Encoding", "identity")
	req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end))
	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0")
	log.Printf("[%d]Start download:%d-%d\n", no, start, end)
	cli := http.Client{
		Timeout: time.Duration(m.TimeOut) * time.Second,
	}
	resp, err := cli.Do(&req)
	if err != nil {
		log.Printf("[%d]ERROR|do request:%s\n", no, err.Error())
		m.redo <- no
		return
	}
	//ranges := resp.Header.Get("Accept-Ranges")

	//log.Printf("[%d]Content-Length:%s\n", i, resp.Header.Get("Content-Length"))
	log.Printf("[%d]Content-Range:%s\n", no, resp.Header.Get("Content-Range"))

	file, err := os.Create(filename)
	if err != nil {
		log.Printf("[%d]ERROR|create file %s:%s\n", no, filename, err.Error())
		file.Close()
		resp.Body.Close()
		m.redo <- no
		return
	}
	log.Printf("[%d]Writing to file %s\n", no, filename)
	StartTime := time.Now()
	//n, err := io.Copy(file, resp.Body)
	response, err := ioutil.ReadAll(resp.Body)
	EndTime := time.Now()
	ration := EndTime.Sub(StartTime).Seconds()
	if err != nil {
		log.Printf("[%d]ERROR|write to file %s:%s,time: %f,len:\n", no, filename, err.Error(), ration, len(response))
		if strings.Contains(err.Error(), "unexpected EOF") {
			goto pars
		}
		file.Close()
		resp.Body.Close()
		m.redo <- no
		return
	}
pars:
	write := bufio.NewWriter(file)
	n, err := write.Write(response)
	if err = write.Flush(); err != nil {
		log.Println("flush err:", err)
	}
	cEndTime := time.Now()
	duration := cEndTime.Sub(cStartTime).Seconds()
	log.Printf("[%d]Download successfully:%f Kb/s\n", no, float64(n)/duration/1024)
	file.Close()
	resp.Body.Close()
	m.finish <- no
	//}
}

func DeepCopy(dst, src interface{}) error {
	var buf bytes.Buffer
	if err := gob.NewEncoder(&buf).Encode(src); err != nil {
		return err
	}
	return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst)
}

/**
 * @Author ChenLin
 * @Description //TODO Url下载视频单线程
 * @Date 10:58 2022/11/11
 **/

func (m *MultiThreadDownload) HttpGetVideoByUrl(videoUrl, dirUrl string) (string, error) {
	res, err := http.Get(videoUrl)
	if err != nil {
		return "", errors.New("HttpGetVideoError")
	}
	defer res.Body.Close()
	//打算MD5一下时间作为随机名称
	fileName := path.Base(utils.MD5(time.Now().String()+strconv.Itoa(rand.Int()))) + ".mp4"
	file, err := os.Create(dirUrl + fileName)
	if err != nil {
		log.Println("Create err", err)
	}
	defer file.Close()
	_, err = io.Copy(file, res.Body)
	if err != nil {
		log.Println("Copy err", err)
		return "", err
	}
	return fmt.Sprintf("%s%s", dirUrl, fileName), nil
}

func (m *MultiThreadDownload) HttpUploadVideo() {

}

/**
 * @Author ChenLin
 * @Description //TODO 判断文件夹、文件是否存在
 * @Date 11:12 2022/11/24
 **/

func (m *MultiThreadDownload) PathExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err != nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, err
}
